/*
 * Copyright (c) 1999, 2013, Oracle and/or its affiliates. All rights reserved.
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS FILE HEADER.
 *
 * This code is free software; you can redistribute it and/or modify it
 * under the terms of the GNU General Public License version 2 only, as
 * published by the Free Software Foundation.  Oracle designates this
 * particular file as subject to the "Classpath" exception as provided
 * by Oracle in the LICENSE file that accompanied this code.
 *
 * This code is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
 * FITNESS FOR A PARTICULAR PURPOSE.  See the GNU General Public License
 * version 2 for more details (a copy is included in the LICENSE file that
 * accompanied this code).
 *
 * You should have received a copy of the GNU General Public License version
 * 2 along with this work; if not, write to the Free Software Foundation,
 * Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA.
 *
 * Please contact Oracle, 500 Oracle Parkway, Redwood Shores, CA 94065 USA
 * or visit www.oracle.com if you need additional information or have any
 * questions.
 */

package org.hotswap.agent.plugin.proxy.java;

import java.io.ByteArrayOutputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.UUID;

import org.hotswap.agent.javassist.ClassPool;
import org.hotswap.agent.javassist.CtClass;
import org.hotswap.agent.javassist.CtMethod;
import org.hotswap.agent.javassist.CtPrimitiveType;
import org.hotswap.agent.javassist.NotFoundException;
import org.hotswap.agent.javassist.bytecode.Descriptor;

/**
 * ProxyGenerator contains the code to generate a dynamic proxy class for the java.lang.reflect.Proxy API.
 *
 * The external interfaces to ProxyGenerator is the static "generateProxyClass" method.
 *
 * @author Peter Jones
 * @since 1.3
 */
/**
 * Converted to use ClassPool and CtClass. Added static field init-code to proxy
 * methods.
 *
 * @author Erki Ehtla
 */
public class CtClassJavaProxyGenerator {
    /*
     * In the comments below, "JVMS" refers to The Java Virtual Machine
     * Specification Second Edition and "JLS" refers to the original version of
     * The Java Language Specification, unless otherwise specified.
     */

    /* generate 1.5-era class file version */
    private static final int CLASSFILE_MAJOR_VERSION = 49;
    private static final int CLASSFILE_MINOR_VERSION = 0;

    /*
     * beginning of constants copied from sun.tools.java.RuntimeConstants (which
     * no longer exists):
     */

    /* constant pool tags */
    private static final int CONSTANT_UTF8 = 1;
    // private static final int CONSTANT_UNICODE = 2;
    private static final int CONSTANT_INTEGER = 3;
    private static final int CONSTANT_FLOAT = 4;
    private static final int CONSTANT_LONG = 5;
    private static final int CONSTANT_DOUBLE = 6;
    private static final int CONSTANT_CLASS = 7;
    private static final int CONSTANT_STRING = 8;
    private static final int CONSTANT_FIELD = 9;
    private static final int CONSTANT_METHOD = 10;
    private static final int CONSTANT_INTERFACEMETHOD = 11;
    private static final int CONSTANT_NAMEANDTYPE = 12;

    /* access and modifier flags */
    private static final int ACC_PUBLIC = 0x00000001;
    private static final int ACC_PRIVATE = 0x00000002;
    // private static final int ACC_PROTECTED = 0x00000004;
    private static final int ACC_STATIC = 0x00000008;
    private static final int ACC_FINAL = 0x00000010;
    // private static final int ACC_SYNCHRONIZED = 0x00000020;
    // private static final int ACC_VOLATILE = 0x00000040;
    // private static final int ACC_TRANSIENT = 0x00000080;
    // private static final int ACC_NATIVE = 0x00000100;
    // private static final int ACC_INTERFACE = 0x00000200;
    // private static final int ACC_ABSTRACT = 0x00000400;
    private static final int ACC_SUPER = 0x00000020;
    // private static final int ACC_STRICT = 0x00000800;

    /* opcodes */
    // private static final int opc_nop = 0;
    private static final int opc_aconst_null = 1;
    // private static final int opc_iconst_m1 = 2;
    private static final int opc_iconst_0 = 3;
    private static final int opc_iconst_1 = 4;
    // private static final int opc_iconst_2 = 5;
    // private static final int opc_iconst_3 = 6;
    // private static final int opc_iconst_4 = 7;
    // private static final int opc_iconst_5 = 8;
    // private static final int opc_lconst_0 = 9;
    // private static final int opc_lconst_1 = 10;
    // private static final int opc_fconst_0 = 11;
    // private static final int opc_fconst_1 = 12;
    // private static final int opc_fconst_2 = 13;
    // private static final int opc_dconst_0 = 14;
    // private static final int opc_dconst_1 = 15;
    private static final int opc_bipush = 16;
    private static final int opc_sipush = 17;
    private static final int opc_ldc = 18;
    private static final int opc_ldc_w = 19;
    // private static final int opc_ldc2_w = 20;
    private static final int opc_iload = 21;
    private static final int opc_lload = 22;
    private static final int opc_fload = 23;
    private static final int opc_dload = 24;
    private static final int opc_aload = 25;
    private static final int opc_iload_0 = 26;
    // private static final int opc_iload_1 = 27;
    // private static final int opc_iload_2 = 28;
    // private static final int opc_iload_3 = 29;
    private static final int opc_lload_0 = 30;
    // private static final int opc_lload_1 = 31;
    // private static final int opc_lload_2 = 32;
    // private static final int opc_lload_3 = 33;
    private static final int opc_fload_0 = 34;
    // private static final int opc_fload_1 = 35;
    // private static final int opc_fload_2 = 36;
    // private static final int opc_fload_3 = 37;
    private static final int opc_dload_0 = 38;
    // private static final int opc_dload_1 = 39;
    // private static final int opc_dload_2 = 40;
    // private static final int opc_dload_3 = 41;
    private static final int opc_aload_0 = 42;
    // private static final int opc_aload_1 = 43;
    // private static final int opc_aload_2 = 44;
    // private static final int opc_aload_3 = 45;
    // private static final int opc_iaload = 46;
    // private static final int opc_laload = 47;
    // private static final int opc_faload = 48;
    // private static final int opc_daload = 49;
    // private static final int opc_aaload = 50;
    // private static final int opc_baload = 51;
    // private static final int opc_caload = 52;
    // private static final int opc_saload = 53;
    // private static final int opc_istore = 54;
    // private static final int opc_lstore = 55;
    // private static final int opc_fstore = 56;
    // private static final int opc_dstore = 57;
    private static final int opc_astore = 58;
    // private static final int opc_istore_0 = 59;
    // private static final int opc_istore_1 = 60;
    // private static final int opc_istore_2 = 61;
    // private static final int opc_istore_3 = 62;
    // private static final int opc_lstore_0 = 63;
    // private static final int opc_lstore_1 = 64;
    // private static final int opc_lstore_2 = 65;
    // private static final int opc_lstore_3 = 66;
    // private static final int opc_fstore_0 = 67;
    // private static final int opc_fstore_1 = 68;
    // private static final int opc_fstore_2 = 69;
    // private static final int opc_fstore_3 = 70;
    // private static final int opc_dstore_0 = 71;
    // private static final int opc_dstore_1 = 72;
    // private static final int opc_dstore_2 = 73;
    // private static final int opc_dstore_3 = 74;
    private static final int opc_astore_0 = 75;
    // private static final int opc_astore_1 = 76;
    // private static final int opc_astore_2 = 77;
    // private static final int opc_astore_3 = 78;
    // private static final int opc_iastore = 79;
    // private static final int opc_lastore = 80;
    // private static final int opc_fastore = 81;
    // private static final int opc_dastore = 82;
    private static final int opc_aastore = 83;
    // private static final int opc_bastore = 84;
    // private static final int opc_castore = 85;
    // private static final int opc_sastore = 86;
    private static final int opc_pop = 87;
    // private static final int opc_pop2 = 88;
    private static final int opc_dup = 89;
    // private static final int opc_dup_x1 = 90;
    // private static final int opc_dup_x2 = 91;
    // private static final int opc_dup2 = 92;
    // private static final int opc_dup2_x1 = 93;
    // private static final int opc_dup2_x2 = 94;
    // private static final int opc_swap = 95;
    // private static final int opc_iadd = 96;
    // private static final int opc_ladd = 97;
    // private static final int opc_fadd = 98;
    // private static final int opc_dadd = 99;
    // private static final int opc_isub = 100;
    // private static final int opc_lsub = 101;
    // private static final int opc_fsub = 102;
    // private static final int opc_dsub = 103;
    // private static final int opc_imul = 104;
    // private static final int opc_lmul = 105;
    // private static final int opc_fmul = 106;
    // private static final int opc_dmul = 107;
    // private static final int opc_idiv = 108;
    // private static final int opc_ldiv = 109;
    // private static final int opc_fdiv = 110;
    // private static final int opc_ddiv = 111;
    // private static final int opc_irem = 112;
    // private static final int opc_lrem = 113;
    // private static final int opc_frem = 114;
    // private static final int opc_drem = 115;
    // private static final int opc_ineg = 116;
    // private static final int opc_lneg = 117;
    // private static final int opc_fneg = 118;
    // private static final int opc_dneg = 119;
    // private static final int opc_ishl = 120;
    // private static final int opc_lshl = 121;
    // private static final int opc_ishr = 122;
    // private static final int opc_lshr = 123;
    // private static final int opc_iushr = 124;
    // private static final int opc_lushr = 125;
    // private static final int opc_iand = 126;
    // private static final int opc_land = 127;
    // private static final int opc_ior = 128;
    // private static final int opc_lor = 129;
    // private static final int opc_ixor = 130;
    // private static final int opc_lxor = 131;
    // private static final int opc_iinc = 132;
    // private static final int opc_i2l = 133;
    // private static final int opc_i2f = 134;
    // private static final int opc_i2d = 135;
    // private static final int opc_l2i = 136;
    // private static final int opc_l2f = 137;
    // private static final int opc_l2d = 138;
    // private static final int opc_f2i = 139;
    // private static final int opc_f2l = 140;
    // private static final int opc_f2d = 141;
    // private static final int opc_d2i = 142;
    // private static final int opc_d2l = 143;
    // private static final int opc_d2f = 144;
    // private static final int opc_i2b = 145;
    // private static final int opc_i2c = 146;
    // private static final int opc_i2s = 147;
    // private static final int opc_lcmp = 148;
    // private static final int opc_fcmpl = 149;
    // private static final int opc_fcmpg = 150;
    // private static final int opc_dcmpl = 151;
    // private static final int opc_dcmpg = 152;
    // private static final int opc_ifeq = 153;
    private static final int opc_ifne = 154;
    // private static final int opc_iflt = 155;
    // private static final int opc_ifge = 156;
    // private static final int opc_ifgt = 157;
    // private static final int opc_ifle = 158;
    // private static final int opc_if_icmpeq = 159;
    // private static final int opc_if_icmpne = 160;
    // private static final int opc_if_icmplt = 161;
    // private static final int opc_if_icmpge = 162;
    // private static final int opc_if_icmpgt = 163;
    // private static final int opc_if_icmple = 164;
    // private static final int opc_if_acmpeq = 165;
    // private static final int opc_if_acmpne = 166;
    // private static final int opc_goto = 167;
    // private static final int opc_jsr = 168;
    // private static final int opc_ret = 169;
    // private static final int opc_tableswitch = 170;
    // private static final int opc_lookupswitch = 171;
    private static final int opc_ireturn = 172;
    private static final int opc_lreturn = 173;
    private static final int opc_freturn = 174;
    private static final int opc_dreturn = 175;
    private static final int opc_areturn = 176;
    private static final int opc_return = 177;
    private static final int opc_getstatic = 178;
    private static final int opc_putstatic = 179;
    private static final int opc_getfield = 180;
    // private static final int opc_putfield = 181;
    private static final int opc_invokevirtual = 182;
    private static final int opc_invokespecial = 183;
    private static final int opc_invokestatic = 184;
    private static final int opc_invokeinterface = 185;
    private static final int opc_new = 187;
    // private static final int opc_newarray = 188;
    private static final int opc_anewarray = 189;
    // private static final int opc_arraylength = 190;
    private static final int opc_athrow = 191;
    private static final int opc_checkcast = 192;
    // private static final int opc_instanceof = 193;
    // private static final int opc_monitorenter = 194;
    // private static final int opc_monitorexit = 195;
    private static final int opc_wide = 196;
    // private static final int opc_multianewarray = 197;
    // private static final int opc_ifnull = 198;
    // private static final int opc_ifnonnull = 199;
    // private static final int opc_goto_w = 200;
    // private static final int opc_jsr_w = 201;

    // end of constants copied from sun.tools.java.RuntimeConstants

    /** name of the superclass of proxy classes */
    private final static String superclassName = "java/lang/reflect/Proxy";

    /** name of field for storing a proxy instance's invocation handler */
    private final static String handlerFieldName = "h";

    /** debugging flag for saving generated class files */
    // private final static boolean saveGeneratedFiles =
    // java.security.AccessController.doPrivileged(
    // new
    // GetBooleanAction("sun.misc.ProxyGenerator.saveGeneratedFiles")).booleanValue();

    /**
     * Generate a public proxy class given a name and a list of proxy
     * interfaces.
     *
     * @param loader
     */
    public static byte[] generateProxyClass(final String name,
            CtClass[] interfaces, ClassPool cp) {
        return generateProxyClass(name, interfaces, cp,
                (ACC_PUBLIC | ACC_FINAL | ACC_SUPER));
    }

    /**
     * Generate a proxy class given a name and a list of proxy interfaces.
     *
     * @param name
     *            the class name of the proxy class
     * @param interfaces
     *            proxy interfaces
     * @param cp
     * @param accessFlags
     *            access flags of the proxy class
     */
    public static byte[] generateProxyClass(final String name,
            CtClass[] interfaces, ClassPool cp, int accessFlags) {
        CtClassJavaProxyGenerator gen = new CtClassJavaProxyGenerator(name,
                interfaces, cp, accessFlags);
        final byte[] classFile = gen.generateClassFile();

        // if (saveGeneratedFiles) {
        // java.security.AccessController.doPrivileged(new
        // java.security.PrivilegedAction<Void>() {
        // public Void run() {
        // try {
        // int i = name.lastIndexOf('.');
        // Path path;
        // if (i > 0) {
        // Path dir = Paths.get(name.substring(0, i).replace('.',
        // File.separatorChar));
        // Files.createDirectories(dir);
        // path = dir.resolve(name.substring(i + 1, name.length()) + ".class");
        // } else {
        // path = Paths.get(name + ".class");
        // }
        // Files.write(path, classFile);
        // return null;
        // } catch (IOException e) {
        // throw new InternalError("I/O exception saving generated file: " + e);
        // }
        // }
        // });
        // }

        return classFile;
    }

    /* preloaded Method objects for methods in java.lang.Object */
    private static class CtClassProxyFields {
        private CtMethod hashCodeMethod;
        private CtMethod equalsMethod;
        private CtMethod toStringMethod;
        private CtClass oclp;
        private CtClass error;
        private CtClass runtimee;
        private CtClass throwable;
    }

    // private static Map<ClassPool, WeakReference<CtClassProxyFields>>
    // fieldsMap = new WeakHashMap<ClassPool,
    // WeakReference<CtClassProxyFields>>(
    // 2);
    private CtClassProxyFields f;

    private void initFields(ClassPool clp) {
        // WeakReference<CtClassProxyFields> ref = fieldsMap.get(clp);
        CtClassProxyFields f = null;
        // if (f == null || (f = ref.get()) == null) {
        // synchronized (fieldsMap) {
        // ref = fieldsMap.get(clp);
        // if (ref == null || (f = ref.get()) == null) {
        f = new CtClassProxyFields();
        try {
            f.oclp = clp.get(Object.class.getName());
            f.error = clp.get(Error.class.getName());
            f.runtimee = clp.get(RuntimeException.class.getName());
            f.throwable = clp.get(Throwable.class.getName());
            f.hashCodeMethod = f.oclp.getDeclaredMethod("hashCode");
            f.equalsMethod = f.oclp.getDeclaredMethod("equals");
            f.toStringMethod = f.oclp.getDeclaredMethod("toString");
        } catch (NotFoundException e) {
            throw new RuntimeException(e);
        }
        // fieldsMap.put(clp, new WeakReference<CtClassProxyFields>(f));
        // }
        // }
        // }
        this.f = f;
    }

    /** name of proxy class */
    private String className;
    private String random = UUID.randomUUID().toString().replace("-", "");
    private String initFieldName = "clinitCalled" + random;
    private String initMethodName = "clinitMethodByJavaAgentForHotSwap";

    /** proxy interfaces */
    private CtClass[] interfaces;

    /** proxy class access flags */
    private int accessFlags;

    /** constant pool of class being generated */
    private ConstantPool cp = new ConstantPool();

    /** FieldInfo struct for each field of generated class */
    private List<FieldInfo> fields = new ArrayList<>();

    /** MethodInfo struct for each method of generated class */
    private List<MethodInfo> methods = new ArrayList<>();

    /**
     * maps method signature string to list of ProxyMethod objects for proxy
     * methods with that signature
     */
    private Map<String, List<ProxyMethod>> proxyMethods = new HashMap<>();

    /** count of ProxyMethod objects added to proxyMethods */
    private int proxyMethodCount = 0;

    /**
     * Construct a ProxyGenerator to generate a proxy class with the specified
     * name and for the given interfaces.
     *
     * A ProxyGenerator object contains the state for the ongoing generation of
     * a particular proxy class.
     *
     * @param cp
     */
    private CtClassJavaProxyGenerator(String className, CtClass[] interfaces,
            ClassPool cp, int accessFlags) {
        this.className = className;
        this.interfaces = interfaces;
        this.accessFlags = accessFlags;
        initFields(cp);
    }

    private void addInterfacesWithSuperInterfaces(
            Collection<CtClass> iWithSuper, CtClass[] interfaces) {
        iWithSuper.addAll(Arrays.asList(interfaces));
        for (CtClass intf : interfaces) {
            try {
                addInterfacesWithSuperInterfaces(iWithSuper,
                        intf.getInterfaces());
            } catch (NotFoundException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * Generate a class file for the proxy class. This method drives the class
     * file generation process.
     */
    private byte[] generateClassFile() {

        /*
         * ============================================================ Step 1:
         * Assemble ProxyMethod objects for all methods to generate proxy
         * dispatching code for.
         */

        /*
         * Record that proxy methods are needed for the hashCode, equals, and
         * toString methods of java.lang.Object. This is done before the methods
         * from the proxy interfaces so that the methods from java.lang.Object
         * take precedence over duplicate methods in the proxy interfaces.
         */
        addProxyMethod(f.hashCodeMethod, f.oclp);
        addProxyMethod(f.equalsMethod, f.oclp);
        addProxyMethod(f.toStringMethod, f.oclp);

        Collection<CtClass> iWithSuper = new HashSet<>();
        addInterfacesWithSuperInterfaces(iWithSuper, interfaces);
        /*
         * Now record all of the methods from the proxy interfaces, giving
         * earlier interfaces precedence over later ones with duplicate methods.
         */
        for (CtClass intf : iWithSuper) {
            for (CtMethod m : intf.getDeclaredMethods()) {
                addProxyMethod(m, intf);
            }
        }

        /*
         * For each set of proxy methods with the same signature, verify that
         * the methods' return types are compatible.
         */
        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            checkReturnTypes(sigmethods);
        }

        /*
         * ============================================================ Step 2:
         * Assemble FieldInfo and MethodInfo structs for all of fields and
         * methods in the class we are generating.
         */
        try {
            methods.add(generateConstructor());

            for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
                for (ProxyMethod pm : sigmethods) {
                    // add static field for method's Method object
                    fields.add(new FieldInfo(pm.methodFieldName,
                            "Ljava/lang/reflect/Method;",
                            ACC_PRIVATE | ACC_STATIC));
                }
            }

            fields.add(new FieldInfo(initFieldName, "Z",
                    ACC_PRIVATE | ACC_STATIC));

            for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
                for (ProxyMethod pm : sigmethods) {
                    // generate code for proxy method and add it
                    methods.add(pm.generateMethod());
                }
            }
            methods.add(generateStaticInitializer());
            methods.add(generateStaticInitializerCaller());

        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }

        if (methods.size() > 65535) {
            throw new IllegalArgumentException("method limit exceeded");
        }
        if (fields.size() > 65535) {
            throw new IllegalArgumentException("field limit exceeded");
        }

        /*
         * ============================================================ Step 3:
         * Write the final class file.
         */

        /*
         * Make sure that constant pool indexes are reserved for the following
         * items before starting to write the final class file.
         */
        cp.getClass(dotToSlash(className));
        cp.getClass(superclassName);
        for (CtClass intf : interfaces) {
            cp.getClass(dotToSlash(intf.getName()));
        }

        /*
         * Disallow new constant pool additions beyond this point, since we are
         * about to write the final constant pool table.
         */
        cp.setReadOnly();

        ByteArrayOutputStream bout = new ByteArrayOutputStream();
        DataOutputStream dout = new DataOutputStream(bout);

        try {
            /*
             * Write all the items of the "ClassFile" structure. See JVMS
             * section 4.1.
             */
            // u4 magic;
            dout.writeInt(0xCAFEBABE);
            // u2 minor_version;
            dout.writeShort(CLASSFILE_MINOR_VERSION);
            // u2 major_version;
            dout.writeShort(CLASSFILE_MAJOR_VERSION);

            cp.write(dout); // (write constant pool)

            // u2 access_flags;
            dout.writeShort(accessFlags);
            // u2 this_class;
            dout.writeShort(cp.getClass(dotToSlash(className)));
            // u2 super_class;
            dout.writeShort(cp.getClass(superclassName));

            // u2 interfaces_count;
            dout.writeShort(interfaces.length);
            // u2 interfaces[interfaces_count];
            for (CtClass intf : interfaces) {
                dout.writeShort(cp.getClass(dotToSlash(intf.getName())));
            }

            // u2 fields_count;
            dout.writeShort(fields.size());
            // field_info fields[fields_count];
            for (FieldInfo f : fields) {
                f.write(dout);
            }

            // u2 methods_count;
            dout.writeShort(methods.size());
            // method_info methods[methods_count];
            for (MethodInfo m : methods) {
                m.write(dout);
            }

            // u2 attributes_count;
            dout.writeShort(0); // (no ClassFile attributes for proxy classes)

        } catch (IOException e) {
            throw new InternalError("unexpected I/O Exception");
        }

        return bout.toByteArray();
    }

    /**
     * Add another method to be proxied, either by creating a new ProxyMethod
     * object or augmenting an old one for a duplicate method.
     *
     * "fromClass" indicates the proxy interface that the method was found
     * through, which may be different from (a subinterface of) the method's
     * "declaring class". Note that the first Method object passed for a given
     * name and descriptor identifies the Method object (and thus the declaring
     * class) that will be passed to the invocation handler's "invoke" method
     * for a given set of duplicate methods.
     */
    private void addProxyMethod(CtMethod m, CtClass fromClass) {
        String name = m.getName();
        CtClass[] parameterTypes;
        CtClass returnType;
        CtClass[] exceptionTypes;
        try {
            parameterTypes = m.getParameterTypes();
            returnType = m.getReturnType();
            exceptionTypes = m.getExceptionTypes();
        } catch (NotFoundException e) {
            throw new RuntimeException(e);
        }

        String sig = name + getParameterDescriptors(parameterTypes);
        List<ProxyMethod> sigmethods = proxyMethods.get(sig);
        if (sigmethods != null) {
            for (ProxyMethod pm : sigmethods) {
                if (returnType == pm.returnType || returnType.getName()
                        .equals(pm.returnType.getName())) {
                    /*
                     * Found a match: reduce exception types to the greatest set
                     * of exceptions that can thrown compatibly with the throws
                     * clauses of both overridden methods.
                     */
                    List<CtClass> legalExceptions = new ArrayList<>();
                    collectCompatibleTypes(exceptionTypes, pm.exceptionTypes,
                            legalExceptions);
                    collectCompatibleTypes(pm.exceptionTypes, exceptionTypes,
                            legalExceptions);
                    pm.exceptionTypes = new CtClass[legalExceptions.size()];
                    pm.exceptionTypes = legalExceptions
                            .toArray(pm.exceptionTypes);
                    return;
                }
            }
        } else {
            sigmethods = new ArrayList<>(3);
            proxyMethods.put(sig, sigmethods);
        }
        sigmethods.add(new ProxyMethod(name, parameterTypes, returnType,
                exceptionTypes, fromClass));
    }

    /**
     * For a given set of proxy methods with the same signature, check that
     * their return types are compatible according to the Proxy specification.
     *
     * Specifically, if there is more than one such method, then all of the
     * return types must be reference types, and there must be one return type
     * that is assignable to each of the rest of them.
     */
    private static void checkReturnTypes(List<ProxyMethod> methods) {
        /*
         * If there is only one method with a given signature, there cannot be a
         * conflict. This is the only case in which a primitive (or void) return
         * type is allowed.
         */
        if (methods.size() < 2) {
            return;
        }

        /*
         * List of return types that are not yet known to be assignable from
         * ("covered" by) any of the others.
         */
        LinkedList<CtClass> uncoveredReturnTypes = new LinkedList<>();

        nextNewReturnType: for (ProxyMethod pm : methods) {
            CtClass newReturnType = pm.returnType;
            if (newReturnType.isPrimitive()) {
                throw new IllegalArgumentException(
                        "methods with same signature "
                                + getFriendlyMethodSignature(pm.methodName,
                                        pm.parameterTypes)
                                + " but incompatible return types: "
                                + newReturnType.getName() + " and others");
            }
            boolean added = false;

            /*
             * Compare the new return type to the existing uncovered return
             * types.
             */
            ListIterator<CtClass> liter = uncoveredReturnTypes.listIterator();
            while (liter.hasNext()) {
                CtClass uncoveredReturnType = liter.next();

                /*
                 * If an existing uncovered return type is assignable to this
                 * new one, then we can forget the new one.
                 */
                if (isAssignableFrom(newReturnType, uncoveredReturnType)) {
                    assert !added;
                    continue nextNewReturnType;
                }

                /*
                 * If the new return type is assignable to an existing uncovered
                 * one, then should replace the existing one with the new one
                 * (or just forget the existing one, if the new one has already
                 * be put in the list).
                 */
                if (isAssignableFrom(uncoveredReturnType, newReturnType)) {
                    // (we can assume that each return type is unique)
                    if (!added) {
                        liter.set(newReturnType);
                        added = true;
                    } else {
                        liter.remove();
                    }
                }
            }

            /*
             * If we got through the list of existing uncovered return types
             * without an assignability relationship, then add the new return
             * type to the list of uncovered ones.
             */
            if (!added) {
                uncoveredReturnTypes.add(newReturnType);
            }
        }

        /*
         * We shouldn't end up with more than one return type that is not
         * assignable from any of the others.
         */
        if (uncoveredReturnTypes.size() > 1) {
            ProxyMethod pm = methods.get(0);
            throw new IllegalArgumentException("methods with same signature "
                    + getFriendlyMethodSignature(pm.methodName,
                            pm.parameterTypes)
                    + " but incompatible return types: "
                    + uncoveredReturnTypes);
        }
    }

    /**
     * A FieldInfo object contains information about a particular field in the
     * class being generated. The class mirrors the data items of the
     * "field_info" structure of the class file format (see JVMS 4.5).
     */
    private class FieldInfo {
        public int accessFlags;
        public String name;
        public String descriptor;

        public FieldInfo(String name, String descriptor, int accessFlags) {
            this.name = name;
            this.descriptor = descriptor;
            this.accessFlags = accessFlags;

            /*
             * Make sure that constant pool indexes are reserved for the
             * following items before starting to write the final class file.
             */
            cp.getUtf8(name);
            cp.getUtf8(descriptor);
        }

        public void write(DataOutputStream out) throws IOException {
            /*
             * Write all the items of the "field_info" structure. See JVMS
             * section 4.5.
             */
            // u2 access_flags;
            out.writeShort(accessFlags);
            // u2 name_index;
            out.writeShort(cp.getUtf8(name));
            // u2 descriptor_index;
            out.writeShort(cp.getUtf8(descriptor));
            // u2 attributes_count;
            out.writeShort(0); // (no field_info attributes for proxy classes)
        }
    }

    /**
     * An ExceptionTableEntry object holds values for the data items of an entry
     * in the "exception_table" item of the "Code" attribute of "method_info"
     * structures (see JVMS 4.7.3).
     */
    private static class ExceptionTableEntry {
        public short startPc;
        public short endPc;
        public short handlerPc;
        public short catchType;

        public ExceptionTableEntry(short startPc, short endPc, short handlerPc,
                short catchType) {
            this.startPc = startPc;
            this.endPc = endPc;
            this.handlerPc = handlerPc;
            this.catchType = catchType;
        }
    };

    /**
     * A MethodInfo object contains information about a particular method in the
     * class being generated. This class mirrors the data items of the
     * "method_info" structure of the class file format (see JVMS 4.6).
     */
    private class MethodInfo {
        public int accessFlags;
        public String name;
        public String descriptor;
        public short maxStack;
        public short maxLocals;
        public ByteArrayOutputStream code = new ByteArrayOutputStream();
        public List<ExceptionTableEntry> exceptionTable = new ArrayList<ExceptionTableEntry>();
        public short[] declaredExceptions;

        public MethodInfo(String name, String descriptor, int accessFlags) {
            this.name = name;
            this.descriptor = descriptor;
            this.accessFlags = accessFlags;

            /*
             * Make sure that constant pool indexes are reserved for the
             * following items before starting to write the final class file.
             */
            cp.getUtf8(name);
            cp.getUtf8(descriptor);
            cp.getUtf8("Code");
            cp.getUtf8("Exceptions");
        }

        public void write(DataOutputStream out) throws IOException {
            /*
             * Write all the items of the "method_info" structure. See JVMS
             * section 4.6.
             */
            // u2 access_flags;
            out.writeShort(accessFlags);
            // u2 name_index;
            out.writeShort(cp.getUtf8(name));
            // u2 descriptor_index;
            out.writeShort(cp.getUtf8(descriptor));
            // u2 attributes_count;
            out.writeShort(2); // (two method_info attributes:)

            // Write "Code" attribute. See JVMS section 4.7.3.

            // u2 attribute_name_index;
            out.writeShort(cp.getUtf8("Code"));
            // u4 attribute_length;
            out.writeInt(12 + code.size() + 8 * exceptionTable.size());
            // u2 max_stack;
            out.writeShort(maxStack);
            // u2 max_locals;
            out.writeShort(maxLocals);
            // u2 code_length;
            out.writeInt(code.size());
            // u1 code[code_length];
            code.writeTo(out);
            // u2 exception_table_length;
            out.writeShort(exceptionTable.size());
            for (ExceptionTableEntry e : exceptionTable) {
                // u2 start_pc;
                out.writeShort(e.startPc);
                // u2 end_pc;
                out.writeShort(e.endPc);
                // u2 handler_pc;
                out.writeShort(e.handlerPc);
                // u2 catch_type;
                out.writeShort(e.catchType);
            }
            // u2 attributes_count;
            out.writeShort(0);

            // write "Exceptions" attribute. See JVMS section 4.7.4.

            // u2 attribute_name_index;
            out.writeShort(cp.getUtf8("Exceptions"));
            // u4 attributes_length;
            out.writeInt(2 + 2 * declaredExceptions.length);
            // u2 number_of_exceptions;
            out.writeShort(declaredExceptions.length);
            // u2 exception_index_table[number_of_exceptions];
            for (short value : declaredExceptions) {
                out.writeShort(value);
            }
        }

    }

    /**
     * A ProxyMethod object represents a proxy method in the proxy class being
     * generated: a method whose implementation will encode and dispatch
     * invocations to the proxy instance's invocation handler.
     */
    private class ProxyMethod {

        /**
         *
         */
        public String methodName;
        public CtClass[] parameterTypes;
        public CtClass returnType;
        public CtClass[] exceptionTypes;
        public CtClass fromClass;
        public String methodFieldName;

        private ProxyMethod(String methodName, CtClass[] parameterTypes,
                CtClass returnType, CtClass[] exceptionTypes,
                CtClass fromClass) {
            this.methodName = methodName;
            this.parameterTypes = parameterTypes;
            this.returnType = returnType;
            this.exceptionTypes = exceptionTypes;
            this.fromClass = fromClass;
            this.methodFieldName = "m" + proxyMethodCount++;
        }

        /**
         * Return a MethodInfo object for this method, including generating the
         * code and exception table entry.
         */
        private MethodInfo generateMethod() throws IOException {
            String desc = getMethodDescriptor(parameterTypes, returnType);
            MethodInfo minfo = new MethodInfo(methodName, desc,
                    ACC_PUBLIC | ACC_FINAL);

            int[] parameterSlot = new int[parameterTypes.length];
            int nextSlot = 1;
            for (int i = 0; i < parameterSlot.length; i++) {
                parameterSlot[i] = nextSlot;
                nextSlot += getWordsPerType(parameterTypes[i]);
            }
            int localSlot0 = nextSlot;
            short pc, tryBegin = 0, tryEnd;

            DataOutputStream out = new DataOutputStream(minfo.code);
            out.writeByte(opc_getstatic);
            out.writeShort(
                    cp.getFieldRef(dotToSlash(className), initFieldName, "Z"));
            out.writeByte(opc_ifne);
            out.writeShort(6);
            out.writeByte(opc_invokestatic);
            out.writeShort(cp.getMethodRef(dotToSlash(className),
                    initMethodName, "()V"));

            code_aload(0, out);

            out.writeByte(opc_getfield);
            out.writeShort(cp.getFieldRef(superclassName, handlerFieldName,
                    "Ljava/lang/reflect/InvocationHandler;"));

            code_aload(0, out);

            out.writeByte(opc_getstatic);
            out.writeShort(cp.getFieldRef(dotToSlash(className),
                    methodFieldName, "Ljava/lang/reflect/Method;"));

            if (parameterTypes.length > 0) {

                code_ipush(parameterTypes.length, out);

                out.writeByte(opc_anewarray);
                out.writeShort(cp.getClass("java/lang/Object"));

                for (int i = 0; i < parameterTypes.length; i++) {

                    out.writeByte(opc_dup);

                    code_ipush(i, out);

                    codeWrapArgument(parameterTypes[i], parameterSlot[i], out);

                    out.writeByte(opc_aastore);
                }
            } else {

                out.writeByte(opc_aconst_null);
            }

            out.writeByte(opc_invokeinterface);
            out.writeShort(cp.getInterfaceMethodRef(
                    "java/lang/reflect/InvocationHandler", "invoke",
                    "(Ljava/lang/Object;Ljava/lang/reflect/Method;"
                            + "[Ljava/lang/Object;)Ljava/lang/Object;"));
            out.writeByte(4);
            out.writeByte(0);

            if (returnType == CtClass.voidType) {

                out.writeByte(opc_pop);

                out.writeByte(opc_return);

            } else {

                codeUnwrapReturnValue(returnType, out);
            }

            tryEnd = pc = (short) minfo.code.size();

            List<CtClass> catchList = computeUniqueCatchList(exceptionTypes);
            if (catchList.size() > 0) {

                for (CtClass ex : catchList) {
                    minfo.exceptionTable.add(new ExceptionTableEntry(tryBegin,
                            tryEnd, pc, cp.getClass(dotToSlash(ex.getName()))));
                }

                out.writeByte(opc_athrow);

                pc = (short) minfo.code.size();

                minfo.exceptionTable.add(new ExceptionTableEntry(tryBegin,
                        tryEnd, pc, cp.getClass("java/lang/Throwable")));

                code_astore(localSlot0, out);

                out.writeByte(opc_new);
                out.writeShort(cp.getClass(
                        "java/lang/reflect/UndeclaredThrowableException"));

                out.writeByte(opc_dup);

                code_aload(localSlot0, out);

                out.writeByte(opc_invokespecial);

                out.writeShort(cp.getMethodRef(
                        "java/lang/reflect/UndeclaredThrowableException",
                        "<init>", "(Ljava/lang/Throwable;)V"));

                out.writeByte(opc_athrow);
            }

            if (minfo.code.size() > 65535) {
                throw new IllegalArgumentException("code size limit exceeded");
            }

            minfo.maxStack = 10;
            minfo.maxLocals = (short) (localSlot0 + 1);
            minfo.declaredExceptions = new short[exceptionTypes.length];
            for (int i = 0; i < exceptionTypes.length; i++) {
                minfo.declaredExceptions[i] = cp
                        .getClass(dotToSlash(exceptionTypes[i].getName()));
            }

            return minfo;
        }

        /**
         * Generate code for wrapping an argument of the given type whose value
         * can be found at the specified local variable index, in order for it
         * to be passed (as an Object) to the invocation handler's "invoke"
         * method. The code is written to the supplied stream.
         */
        private void codeWrapArgument(CtClass type, int slot,
                DataOutputStream out) throws IOException {
            if (type.isPrimitive()) {
                PrimitiveTypeInfo prim = PrimitiveTypeInfo.get(type);

                if (type == CtClass.intType || type == CtClass.booleanType
                        || type == CtClass.byteType || type == CtClass.charType
                        || type == CtClass.shortType) {
                    code_iload(slot, out);
                } else if (type == CtClass.longType) {
                    code_lload(slot, out);
                } else if (type == CtClass.floatType) {
                    code_fload(slot, out);
                } else if (type == CtClass.doubleType) {
                    code_dload(slot, out);
                } else {
                    throw new AssertionError();
                }

                out.writeByte(opc_invokestatic);
                out.writeShort(cp.getMethodRef(prim.wrapperClassName, "valueOf",
                        prim.wrapperValueOfDesc));

            } else {

                code_aload(slot, out);
            }
        }

        /**
         * Generate code for unwrapping a return value of the given type from
         * the invocation handler's "invoke" method (as type Object) to its
         * correct type. The code is written to the supplied stream.
         */
        private void codeUnwrapReturnValue(CtClass type, DataOutputStream out)
                throws IOException {
            if (type.isPrimitive()) {
                PrimitiveTypeInfo prim = PrimitiveTypeInfo.get(type);

                out.writeByte(opc_checkcast);
                out.writeShort(cp.getClass(prim.wrapperClassName));

                out.writeByte(opc_invokevirtual);
                out.writeShort(cp.getMethodRef(prim.wrapperClassName,
                        prim.unwrapMethodName, prim.unwrapMethodDesc));

                if (type == CtClass.intType || type == CtClass.booleanType
                        || type == CtClass.byteType || type == CtClass.charType
                        || type == CtClass.shortType) {
                    out.writeByte(opc_ireturn);
                } else if (type == CtClass.longType) {
                    out.writeByte(opc_lreturn);
                } else if (type == CtClass.floatType) {
                    out.writeByte(opc_freturn);
                } else if (type == CtClass.doubleType) {
                    out.writeByte(opc_dreturn);
                } else {
                    throw new AssertionError();
                }

            } else {

                out.writeByte(opc_checkcast);
                out.writeShort(cp.getClass(dotToSlash(type.getName())));

                out.writeByte(opc_areturn);
            }
        }

        /**
         * Generate code for initializing the static field that stores the
         * Method object for this proxy method. The code is written to the
         * supplied stream.
         */
        private void codeFieldInitialization(DataOutputStream out)
                throws IOException {
            codeClassForName(fromClass, out);

            code_ldc(cp.getString(methodName), out);

            code_ipush(parameterTypes.length, out);

            out.writeByte(opc_anewarray);
            out.writeShort(cp.getClass("java/lang/Class"));

            for (int i = 0; i < parameterTypes.length; i++) {

                out.writeByte(opc_dup);

                code_ipush(i, out);

                if (parameterTypes[i].isPrimitive()) {
                    PrimitiveTypeInfo prim = PrimitiveTypeInfo
                            .get(parameterTypes[i]);

                    out.writeByte(opc_getstatic);
                    out.writeShort(cp.getFieldRef(prim.wrapperClassName, "TYPE",
                            "Ljava/lang/Class;"));

                } else {
                    codeClassForName(parameterTypes[i], out);
                }

                out.writeByte(opc_aastore);
            }

            out.writeByte(opc_invokevirtual);
            out.writeShort(cp.getMethodRef("java/lang/Class", "getMethod",
                    "(Ljava/lang/String;[Ljava/lang/Class;)"
                            + "Ljava/lang/reflect/Method;"));

            out.writeByte(opc_putstatic);
            out.writeShort(cp.getFieldRef(dotToSlash(className),
                    methodFieldName, "Ljava/lang/reflect/Method;"));
        }
    }

    /**
     * Generate the constructor method for the proxy class.
     */
    private MethodInfo generateConstructor() throws IOException {
        MethodInfo minfo = new MethodInfo("<init>",
                "(Ljava/lang/reflect/InvocationHandler;)V", ACC_PUBLIC);

        DataOutputStream out = new DataOutputStream(minfo.code);

        code_aload(0, out);

        code_aload(1, out);

        out.writeByte(opc_invokespecial);
        out.writeShort(cp.getMethodRef(superclassName, "<init>",
                "(Ljava/lang/reflect/InvocationHandler;)V"));

        out.writeByte(opc_return);

        minfo.maxStack = 10;
        minfo.maxLocals = 2;
        minfo.declaredExceptions = new short[0];

        return minfo;
    }

    /**
     * Generate the static initializer method for the proxy class.
     */
    private MethodInfo generateStaticInitializerCaller() throws IOException {
        MethodInfo minfo = new MethodInfo("<clinit>", "()V", ACC_STATIC);

        DataOutputStream out = new DataOutputStream(minfo.code);
        out.writeByte(opc_invokestatic);
        out.writeShort(
                cp.getMethodRef(dotToSlash(className), initMethodName, "()V"));
        out.writeByte(opc_return);

        minfo.declaredExceptions = new short[0];
        return minfo;
    }

    /**
     * Generate the static initializer method for the proxy class.
     */
    private MethodInfo generateStaticInitializer() throws IOException {
        MethodInfo minfo = new MethodInfo(initMethodName, "()V", ACC_STATIC);

        int localSlot0 = 1;
        short pc, tryBegin = 0, tryEnd;

        DataOutputStream out = new DataOutputStream(minfo.code);

        for (List<ProxyMethod> sigmethods : proxyMethods.values()) {
            for (ProxyMethod pm : sigmethods) {
                pm.codeFieldInitialization(out);
            }
        }

        out.writeByte(opc_iconst_1);
        out.writeByte(opc_putstatic);
        out.writeShort(
                cp.getFieldRef(dotToSlash(className), initFieldName, "Z"));

        out.writeByte(opc_return);

        tryEnd = pc = (short) minfo.code.size();

        minfo.exceptionTable.add(new ExceptionTableEntry(tryBegin, tryEnd, pc,
                cp.getClass("java/lang/NoSuchMethodException")));

        code_astore(localSlot0, out);

        out.writeByte(opc_new);
        out.writeShort(cp.getClass("java/lang/NoSuchMethodError"));

        out.writeByte(opc_dup);

        code_aload(localSlot0, out);

        out.writeByte(opc_invokevirtual);
        out.writeShort(cp.getMethodRef("java/lang/Throwable", "getMessage",
                "()Ljava/lang/String;"));

        out.writeByte(opc_invokespecial);
        out.writeShort(cp.getMethodRef("java/lang/NoSuchMethodError", "<init>",
                "(Ljava/lang/String;)V"));

        out.writeByte(opc_athrow);

        pc = (short) minfo.code.size();

        minfo.exceptionTable.add(new ExceptionTableEntry(tryBegin, tryEnd, pc,
                cp.getClass("java/lang/ClassNotFoundException")));

        code_astore(localSlot0, out);

        out.writeByte(opc_new);
        out.writeShort(cp.getClass("java/lang/NoClassDefFoundError"));

        out.writeByte(opc_dup);

        code_aload(localSlot0, out);

        out.writeByte(opc_invokevirtual);
        out.writeShort(cp.getMethodRef("java/lang/Throwable", "getMessage",
                "()Ljava/lang/String;"));

        out.writeByte(opc_invokespecial);
        out.writeShort(cp.getMethodRef("java/lang/NoClassDefFoundError",
                "<init>", "(Ljava/lang/String;)V"));

        out.writeByte(opc_athrow);

        if (minfo.code.size() > 65535) {
            throw new IllegalArgumentException("code size limit exceeded");
        }

        minfo.maxStack = 10;
        minfo.maxLocals = (short) (localSlot0 + 1);
        minfo.declaredExceptions = new short[0];

        return minfo;
    }

    /*
     * =============== Code Generation Utility Methods ===============
     */

    /*
     * The following methods generate code for the load or store operation
     * indicated by their name for the given local variable. The code is written
     * to the supplied stream.
     */

    private void code_iload(int lvar, DataOutputStream out) throws IOException {
        codeLocalLoadStore(lvar, opc_iload, opc_iload_0, out);
    }

    private void code_lload(int lvar, DataOutputStream out) throws IOException {
        codeLocalLoadStore(lvar, opc_lload, opc_lload_0, out);
    }

    private void code_fload(int lvar, DataOutputStream out) throws IOException {
        codeLocalLoadStore(lvar, opc_fload, opc_fload_0, out);
    }

    private void code_dload(int lvar, DataOutputStream out) throws IOException {
        codeLocalLoadStore(lvar, opc_dload, opc_dload_0, out);
    }

    private void code_aload(int lvar, DataOutputStream out) throws IOException {
        codeLocalLoadStore(lvar, opc_aload, opc_aload_0, out);
    }

    // private void code_istore(int lvar, DataOutputStream out)
    // throws IOException
    // {
    // codeLocalLoadStore(lvar, opc_istore, opc_istore_0, out);
    // }

    // private void code_lstore(int lvar, DataOutputStream out)
    // throws IOException
    // {
    // codeLocalLoadStore(lvar, opc_lstore, opc_lstore_0, out);
    // }

    // private void code_fstore(int lvar, DataOutputStream out)
    // throws IOException
    // {
    // codeLocalLoadStore(lvar, opc_fstore, opc_fstore_0, out);
    // }

    // private void code_dstore(int lvar, DataOutputStream out)
    // throws IOException
    // {
    // codeLocalLoadStore(lvar, opc_dstore, opc_dstore_0, out);
    // }

    private void code_astore(int lvar, DataOutputStream out)
            throws IOException {
        codeLocalLoadStore(lvar, opc_astore, opc_astore_0, out);
    }

    /**
     * Generate code for a load or store instruction for the given local
     * variable. The code is written to the supplied stream.
     *
     * "opcode" indicates the opcode form of the desired load or store
     * instruction that takes an explicit local variable index, and "opcode_0"
     * indicates the corresponding form of the instruction with the implicit
     * index 0.
     */
    private void codeLocalLoadStore(int lvar, int opcode, int opcode_0,
            DataOutputStream out) throws IOException {
        assert lvar >= 0 && lvar <= 0xFFFF;
        if (lvar <= 3) {
            out.writeByte(opcode_0 + lvar);
        } else if (lvar <= 0xFF) {
            out.writeByte(opcode);
            out.writeByte(lvar & 0xFF);
        } else {
            /*
             * Use the "wide" instruction modifier for local variable indexes
             * that do not fit into an unsigned byte.
             */
            out.writeByte(opc_wide);
            out.writeByte(opcode);
            out.writeShort(lvar & 0xFFFF);
        }
    }

    /**
     * Generate code for an "ldc" instruction for the given constant pool index
     * (the "ldc_w" instruction is used if the index does not fit into an
     * unsigned byte). The code is written to the supplied stream.
     */
    private void code_ldc(int index, DataOutputStream out) throws IOException {
        assert index >= 0 && index <= 0xFFFF;
        if (index <= 0xFF) {
            out.writeByte(opc_ldc);
            out.writeByte(index & 0xFF);
        } else {
            out.writeByte(opc_ldc_w);
            out.writeShort(index & 0xFFFF);
        }
    }

    /**
     * Generate code to push a constant integer value on to the operand stack,
     * using the "iconst_<i>", "bipush", or "sipush" instructions depending on
     * the size of the value. The code is written to the supplied stream.
     */
    private void code_ipush(int value, DataOutputStream out)
            throws IOException {
        if (value >= -1 && value <= 5) {
            out.writeByte(opc_iconst_0 + value);
        } else if (value >= Byte.MIN_VALUE && value <= Byte.MAX_VALUE) {
            out.writeByte(opc_bipush);
            out.writeByte(value & 0xFF);
        } else if (value >= Short.MIN_VALUE && value <= Short.MAX_VALUE) {
            out.writeByte(opc_sipush);
            out.writeShort(value & 0xFFFF);
        } else {
            throw new AssertionError();
        }
    }

    /**
     * Generate code to invoke the Class.forName with the name of the given
     * class to get its Class object at runtime. The code is written to the
     * supplied stream. Note that the code generated by this method may caused
     * the checked ClassNotFoundException to be thrown.
     */
    private void codeClassForName(CtClass cl, DataOutputStream out)
            throws IOException {
        if (cl.isArray()) {
            code_ldc(cp.getString(Descriptor.of(cl).replace("/", ".")), out);
        } else
            code_ldc(cp.getString(cl.getName()), out);

        out.writeByte(opc_invokestatic);
        out.writeShort(cp.getMethodRef("java/lang/Class", "forName",
                "(Ljava/lang/String;)Ljava/lang/Class;"));
    }

    /*
     * ==================== General Utility Methods ====================
     */

    /**
     * Convert a fully qualified class name that uses '.' as the package
     * separator, the external representation used by the Java language and
     * APIs, to a fully qualified class name that uses '/' as the package
     * separator, the representation used in the class file format (see JVMS
     * section 4.2).
     */
    private static String dotToSlash(String name) {
        return name.replace('.', '/');
    }

    /**
     * Return the "method descriptor" string for a method with the given
     * parameter types and return type. See JVMS section 4.3.3.
     */
    private static String getMethodDescriptor(CtClass[] parameterTypes,
            CtClass returnType) {
        return getParameterDescriptors(parameterTypes)
                + ((returnType == CtClass.voidType) ? "V"
                        : getFieldType(returnType));
    }

    /**
     * Return the list of "parameter descriptor" strings enclosed in parentheses
     * corresponding to the given parameter types (in other words, a method
     * descriptor without a return descriptor). This string is useful for
     * constructing string keys for methods without regard to their return type.
     */
    private static String getParameterDescriptors(CtClass[] parameterTypes) {
        StringBuilder desc = new StringBuilder("(");
        for (int i = 0; i < parameterTypes.length; i++) {
            desc.append(getFieldType(parameterTypes[i]));
        }
        desc.append(')');
        return desc.toString();
    }

    /**
     * Return the "field type" string for the given type, appropriate for a
     * field descriptor, a parameter descriptor, or a return descriptor other
     * than "void". See JVMS section 4.3.2.
     */
    private static String getFieldType(CtClass type) {
        if (type.isPrimitive()) {
            return PrimitiveTypeInfo.get(type).baseTypeString;
        } else if (type.isArray()) {
            /*
             * According to JLS 20.3.2, the getName() method on Class does
             * return the VM type descriptor format for array classes (only);
             * using that should be quicker than the otherwise obvious code:
             *
             * return "[" + getTypeDescriptor(type.getComponentType());
             */
            return Descriptor.of(type);
        } else {
            return "L" + dotToSlash(type.getName()) + ";";
        }
    }

    /**
     * Returns a human-readable string representing the signature of a method
     * with the given name and parameter types.
     */
    private static String getFriendlyMethodSignature(String name,
            CtClass[] parameterTypes) {
        StringBuilder sig = new StringBuilder(name);
        sig.append('(');
        for (int i = 0; i < parameterTypes.length; i++) {
            if (i > 0) {
                sig.append(',');
            }
            CtClass parameterType = parameterTypes[i];
            int dimensions = 0;
            while (parameterType.isArray()) {
                try {
                    parameterType = parameterType.getComponentType();
                } catch (NotFoundException e) {
                    throw new RuntimeException(e);
                }
                dimensions++;
            }
            sig.append(parameterType.getName());
            while (dimensions-- > 0) {
                sig.append("[]");
            }
        }
        sig.append(')');
        return sig.toString();
    }

    /**
     * Return the number of abstract "words", or consecutive local variable
     * indexes, required to contain a value of the given type. See JVMS section
     * 3.6.1.
     *
     * Note that the original version of the JVMS contained a definition of this
     * abstract notion of a "word" in section 3.4, but that definition was
     * removed for the second edition.
     */
    private static int getWordsPerType(CtClass type) {
        if (type == CtClass.longType || type == CtClass.doubleType) {
            return 2;
        } else {
            return 1;
        }
    }

    /**
     * Add to the given list all of the types in the "from" array that are not
     * already contained in the list and are assignable to at least one of the
     * types in the "with" array.
     *
     * This method is useful for computing the greatest common set of declared
     * exceptions from duplicate methods inherited from different interfaces.
     */
    private static void collectCompatibleTypes(CtClass[] from, CtClass[] with,
            List<CtClass> list) {
        for (CtClass fc : from) {
            if (!list.contains(fc)) {
                for (CtClass wc : with) {
                    if (isAssignableFrom(wc, fc)) {
                        list.add(fc);
                        break;
                    }
                }
            }
        }
    }

    /**
     * Given the exceptions declared in the throws clause of a proxy method,
     * compute the exceptions that need to be caught from the invocation
     * handler's invoke method and rethrown intact in the method's
     * implementation before catching other Throwables and wrapping them in
     * UndeclaredThrowableExceptions.
     *
     * The exceptions to be caught are returned in a List object. Each exception
     * in the returned list is guaranteed to not be a subclass of any of the
     * other exceptions in the list, so the catch blocks for these exceptions
     * may be generated in any order relative to each other.
     *
     * Error and RuntimeException are each always contained by the returned list
     * (if none of their superclasses are contained), since those unchecked
     * exceptions should always be rethrown intact, and thus their subclasses
     * will never appear in the returned list.
     *
     * The returned List will be empty if java.lang.Throwable is in the given
     * list of declared exceptions, indicating that no exceptions need to be
     * caught.
     */
    private List<CtClass> computeUniqueCatchList(CtClass[] exceptions) {
        List<CtClass> uniqueList = new ArrayList<>();
        // unique exceptions to catch

        uniqueList.add(f.error); // always catch/rethrow these
        uniqueList.add(f.runtimee);

        nextException: for (CtClass ex : exceptions) {
            if (isAssignableFrom(ex, f.throwable)) {
                /*
                 * If Throwable is declared to be thrown by the proxy method,
                 * then no catch blocks are necessary, because the invoke can,
                 * at most, throw Throwable anyway.
                 */
                uniqueList.clear();
                break;
            } else if (!isAssignableFrom(f.throwable, ex)) {
                /*
                 * Ignore types that cannot be thrown by the invoke method.
                 */
                continue;
            }
            /*
             * Compare this exception against the current list of exceptions
             * that need to be caught:
             */
            for (int j = 0; j < uniqueList.size();) {
                CtClass ex2 = uniqueList.get(j);
                if (isAssignableFrom(ex2, ex)) {
                    /*
                     * if a superclass of this exception is already on the list
                     * to catch, then ignore this one and continue;
                     */
                    continue nextException;
                } else if (isAssignableFrom(ex, ex2)) {
                    /*
                     * if a subclass of this exception is on the list to catch,
                     * then remove it;
                     */
                    uniqueList.remove(j);
                } else {
                    j++; // else continue comparing.
                }
            }
            // This exception is unique (so far): add it to the list to catch.
            uniqueList.add(ex);
        }
        return uniqueList;
    }

    /**
     * @param ex2
     * @param ex
     * @return
     */
    private static boolean isAssignableFrom(CtClass ex2, CtClass ex) {
        try {
            return ex.subtypeOf(ex2);
        } catch (NotFoundException e) {
            throw new RuntimeException(e);
        }
    }

    /**
     * A PrimitiveTypeInfo object contains assorted information about a
     * primitive type in its public fields. The struct for a particular
     * primitive type can be obtained using the static "get" method.
     */
    private static class PrimitiveTypeInfo {

        /** "base type" used in various descriptors (see JVMS section 4.3.2) */
        public String baseTypeString;

        /** name of corresponding wrapper class */
        public String wrapperClassName;

        /** method descriptor for wrapper class "valueOf" factory method */
        public String wrapperValueOfDesc;

        /** name of wrapper class method for retrieving primitive value */
        public String unwrapMethodName;

        /** descriptor of same method */
        public String unwrapMethodDesc;

        private static Map<CtClass, PrimitiveTypeInfo> table = new HashMap<>();
        // static {
        // add(byte.class, Byte.class);
        // add(char.class, Character.class);
        // add(double.class, Double.class);
        // add(float.class, Float.class);
        // add(int.class, Integer.class);
        // add(long.class, Long.class);
        // add(short.class, Short.class);
        // add(boolean.class, Boolean.class);
        // }

        static {
            add(CtClass.byteType);
            add(CtClass.charType);
            add(CtClass.doubleType);
            add(CtClass.floatType);
            add(CtClass.intType);
            add(CtClass.longType);
            add(CtClass.shortType);
            add(CtClass.booleanType);
        }

        private static void add(CtClass primitiveClass) {
            table.put(primitiveClass, new PrimitiveTypeInfo(primitiveClass));
        }

        private PrimitiveTypeInfo(CtClass primitiveClass) {
            assert primitiveClass.isPrimitive();

            // try {
            // baseTypeString = Array.newInstance(primitiveClass.toClass(),
            // 0).getClass().getName().substring(1);
            // } catch (NegativeArraySizeException | CannotCompileException e) {
            // throw new RuntimeException();
            // }
            baseTypeString = String.valueOf(
                    ((CtPrimitiveType) primitiveClass).getDescriptor());
            wrapperClassName = dotToSlash(
                    ((CtPrimitiveType) primitiveClass).getWrapperName());
            wrapperValueOfDesc = "(" + baseTypeString + ")L" + wrapperClassName
                    + ";";
            unwrapMethodName = primitiveClass.getName() + "Value";
            unwrapMethodDesc = "()" + baseTypeString;
        }

        public static PrimitiveTypeInfo get(CtClass cl) {
            return table.get(cl);
        }
    }

    /**
     * A ConstantPool object represents the constant pool of a class file being
     * generated. This representation of a constant pool is designed
     * specifically for use by ProxyGenerator; in particular, it assumes that
     * constant pool entries will not need to be resorted (for example, by their
     * type, as the Java compiler does), so that the final index value can be
     * assigned and used when an entry is first created.
     *
     * Note that new entries cannot be created after the constant pool has been
     * written to a class file. To prevent such logic errors, a ConstantPool
     * instance can be marked "read only", so that further attempts to add new
     * entries will fail with a runtime exception.
     *
     * See JVMS section 4.4 for more information about the constant pool of a
     * class file.
     */
    private static class ConstantPool {

        /**
         * list of constant pool entries, in constant pool index order.
         *
         * This list is used when writing the constant pool to a stream and for
         * assigning the next index value. Note that element 0 of this list
         * corresponds to constant pool index 1.
         */
        private List<Entry> pool = new ArrayList<>(32);

        /**
         * maps constant pool data of all types to constant pool indexes.
         *
         * This map is used to look up the index of an existing entry for values
         * of all types.
         */
        private Map<Object, Short> map = new HashMap<>(16);

        /** true if no new constant pool entries may be added */
        private boolean readOnly = false;

        /**
         * Get or assign the index for a CONSTANT_Utf8 entry.
         */
        public short getUtf8(String s) {
            if (s == null) {
                throw new NullPointerException();
            }
            return getValue(s);
        }

        // /**
        // * Get or assign the index for a CONSTANT_Integer entry.
        // */
        // public short getInteger(int i) {
        // return getValue(new Integer(i));
        // }
        //
        // /**
        // * Get or assign the index for a CONSTANT_Float entry.
        // */
        // public short getFloat(float f) {
        // return getValue(new Float(f));
        // }

        /**
         * Get or assign the index for a CONSTANT_Class entry.
         */
        public short getClass(String name) {
            short utf8Index = getUtf8(name);
            return getIndirect(new IndirectEntry(CONSTANT_CLASS, utf8Index));
        }

        /**
         * Get or assign the index for a CONSTANT_String entry.
         */
        public short getString(String s) {
            short utf8Index = getUtf8(s);
            return getIndirect(new IndirectEntry(CONSTANT_STRING, utf8Index));
        }

        /**
         * Get or assign the index for a CONSTANT_FieldRef entry.
         */
        public short getFieldRef(String className, String name,
                String descriptor) {
            short classIndex = getClass(className);
            short nameAndTypeIndex = getNameAndType(name, descriptor);
            return getIndirect(new IndirectEntry(CONSTANT_FIELD, classIndex,
                    nameAndTypeIndex));
        }

        /**
         * Get or assign the index for a CONSTANT_MethodRef entry.
         */
        public short getMethodRef(String className, String name,
                String descriptor) {
            short classIndex = getClass(className);
            short nameAndTypeIndex = getNameAndType(name, descriptor);
            return getIndirect(new IndirectEntry(CONSTANT_METHOD, classIndex,
                    nameAndTypeIndex));
        }

        /**
         * Get or assign the index for a CONSTANT_InterfaceMethodRef entry.
         */
        public short getInterfaceMethodRef(String className, String name,
                String descriptor) {
            short classIndex = getClass(className);
            short nameAndTypeIndex = getNameAndType(name, descriptor);
            return getIndirect(new IndirectEntry(CONSTANT_INTERFACEMETHOD,
                    classIndex, nameAndTypeIndex));
        }

        /**
         * Get or assign the index for a CONSTANT_NameAndType entry.
         */
        public short getNameAndType(String name, String descriptor) {
            short nameIndex = getUtf8(name);
            short descriptorIndex = getUtf8(descriptor);
            return getIndirect(new IndirectEntry(CONSTANT_NAMEANDTYPE,
                    nameIndex, descriptorIndex));
        }

        /**
         * Set this ConstantPool instance to be "read only".
         *
         * After this method has been called, further requests to get an index
         * for a non-existent entry will cause an InternalError to be thrown
         * instead of creating of the entry.
         */
        public void setReadOnly() {
            readOnly = true;
        }

        /**
         * Write this constant pool to a stream as part of the class file
         * format.
         *
         * This consists of writing the "constant_pool_count" and
         * "constant_pool[]" items of the "ClassFile" structure, as described in
         * JVMS section 4.1.
         */
        public void write(OutputStream out) throws IOException {
            DataOutputStream dataOut = new DataOutputStream(out);

            // constant_pool_count: number of entries plus one
            dataOut.writeShort(pool.size() + 1);

            for (Entry e : pool) {
                e.write(dataOut);
            }
        }

        /**
         * Add a new constant pool entry and return its index.
         */
        private short addEntry(Entry entry) {
            pool.add(entry);
            /*
             * Note that this way of determining the index of the added entry is
             * wrong if this pool supports CONSTANT_Long or CONSTANT_Double
             * entries.
             */
            if (pool.size() >= 65535) {
                throw new IllegalArgumentException(
                        "constant pool size limit exceeded");
            }
            return (short) pool.size();
        }

        /**
         * Get or assign the index for an entry of a type that contains a direct
         * value. The type of the given object determines the type of the
         * desired entry as follows:
         *
         * java.lang.String CONSTANT_Utf8 java.lang.Integer CONSTANT_Integer
         * java.lang.Float CONSTANT_Float java.lang.Long CONSTANT_Long
         * java.lang.Double CONSTANT_DOUBLE
         */
        private short getValue(Object key) {
            Short index = map.get(key);
            if (index != null) {
                return index.shortValue();
            } else {
                if (readOnly) {
                    throw new InternalError(
                            "late constant pool addition: " + key);
                }
                short i = addEntry(new ValueEntry(key));
                map.put(key, new Short(i));
                return i;
            }
        }

        /**
         * Get or assign the index for an entry of a type that contains
         * references to other constant pool entries.
         */
        private short getIndirect(IndirectEntry e) {
            Short index = map.get(e);
            if (index != null) {
                return index.shortValue();
            } else {
                if (readOnly) {
                    throw new InternalError("late constant pool addition");
                }
                short i = addEntry(e);
                map.put(e, new Short(i));
                return i;
            }
        }

        /**
         * Entry is the abstact superclass of all constant pool entry types that
         * can be stored in the "pool" list; its purpose is to define a common
         * method for writing constant pool entries to a class file.
         */
        private static abstract class Entry {
            public abstract void write(DataOutputStream out) throws IOException;
        }

        /**
         * ValueEntry represents a constant pool entry of a type that contains a
         * direct value (see the comments for the "getValue" method for a list
         * of such types).
         *
         * ValueEntry objects are not used as keys for their entries in the Map
         * "map", so no useful hashCode or equals methods are defined.
         */
        private static class ValueEntry extends Entry {
            private Object value;

            public ValueEntry(Object value) {
                this.value = value;
            }

            public void write(DataOutputStream out) throws IOException {
                if (value instanceof String) {
                    out.writeByte(CONSTANT_UTF8);
                    out.writeUTF((String) value);
                } else if (value instanceof Integer) {
                    out.writeByte(CONSTANT_INTEGER);
                    out.writeInt(((Integer) value).intValue());
                } else if (value instanceof Float) {
                    out.writeByte(CONSTANT_FLOAT);
                    out.writeFloat(((Float) value).floatValue());
                } else if (value instanceof Long) {
                    out.writeByte(CONSTANT_LONG);
                    out.writeLong(((Long) value).longValue());
                } else if (value instanceof Double) {
                    out.writeDouble(CONSTANT_DOUBLE);
                    out.writeDouble(((Double) value).doubleValue());
                } else {
                    throw new InternalError("bogus value entry: " + value);
                }
            }
        }

        /**
         * IndirectEntry represents a constant pool entry of a type that
         * references other constant pool entries, i.e., the following types:
         *
         * CONSTANT_Class, CONSTANT_String, CONSTANT_Fieldref,
         * CONSTANT_Methodref, CONSTANT_InterfaceMethodref, and
         * CONSTANT_NameAndType.
         *
         * Each of these entry types contains either one or two indexes of other
         * constant pool entries.
         *
         * IndirectEntry objects are used as the keys for their entries in the
         * Map "map", so the hashCode and equals methods are overridden to allow
         * matching.
         */
        private static class IndirectEntry extends Entry {
            private int tag;
            private short index0;
            private short index1;

            /**
             * Construct an IndirectEntry for a constant pool entry type that
             * contains one index of another entry.
             */
            public IndirectEntry(int tag, short index) {
                this.tag = tag;
                this.index0 = index;
                this.index1 = 0;
            }

            /**
             * Construct an IndirectEntry for a constant pool entry type that
             * contains two indexes for other entries.
             */
            public IndirectEntry(int tag, short index0, short index1) {
                this.tag = tag;
                this.index0 = index0;
                this.index1 = index1;
            }

            public void write(DataOutputStream out) throws IOException {
                out.writeByte(tag);
                out.writeShort(index0);
                /*
                 * If this entry type contains two indexes, write out the
                 * second, too.
                 */
                if (tag == CONSTANT_FIELD || tag == CONSTANT_METHOD
                        || tag == CONSTANT_INTERFACEMETHOD
                        || tag == CONSTANT_NAMEANDTYPE) {
                    out.writeShort(index1);
                }
            }

            public int hashCode() {
                return tag + index0 + index1;
            }

            public boolean equals(Object obj) {
                if (obj instanceof IndirectEntry) {
                    IndirectEntry other = (IndirectEntry) obj;
                    if (tag == other.tag && index0 == other.index0
                            && index1 == other.index1) {
                        return true;
                    }
                }
                return false;
            }
        }
    }
}
